iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 17

Day 17: Vue-非受控元件認識、列表渲染(v-for)的陷阱

  • 分享至 

  • xImage
  •  

上次介紹插槽時有看到v-for渲染的案例,v-for是Vue官方提供的列表渲染(List rendering)指令之一,可以用來遍歷陣列或對象,在模板中生成一组相同結構的元素,使用起來滿簡單,但是實務常常看到一些的誤區使用和陷阱。在撰寫文章資源的同時,也發現到一些受控和非受控元件的觀念沒有很清楚,希望也能夠一併統合觀念,打破一些不了解模糊的部分。

今日學習目標:

  1. 理解Vue 就地更新策略(in place patch)
  2. 非受控元素觀念認識(uncontrolled component)-認識樣板ref
  3. v-for不綁定key值的缺點
  4. 使用 index 作為 key 的缺點

Vue 就地更新策略(in place patch)

就地更新(in-place patch)指的是v-for 列表渲染的元素列表中,Vue 會選擇不重新創建或重新排列 DOM 元素的情况下,只更新現有 DOM 元素的綁定的資料内容,減少不必要的 DOM 操作移動進而提高效率

  1. 就地更新,如果只有預設綁定的響應式系統資料變動,DOM本身順序結構不動的話,只替換資料。

  2. 對於一個陣列物物件列表有順序變更(ex: 排序變化),Vue 會盡可能記取元素對應位置,重複使用原本的 DOM 元素,而不是移除舊元素並建立新元素再渲染資料。

這意味著Vue預設只會將新資料套用到原本的 DOM 元素上,而不會調整 DOM 中元素的順序,也不會連動由DOM元素本身控制的狀態,特別是非受控組控制的資料流的狀態(uncontrolled component)


什麼是非受控

  • 操作案例:
  1. 在表單中如果你直接依賴原生 DOM 的 input 元素來追蹤使用者的輸入,而不是通過 Vue 的響應式狀態來更新或管理這些輸入,那麼該元件就被認為是非受控的。
  2. 像是input元素有一些聚焦(focous)、滾軸功能(scroll behaviour),這些由瀏覽器Web API提供的功能對原生 DOM 元素的即時操作,本身並不在Vue所提供的API下。

template ref(樣板ref)

為了達成操作Vue本身沒有瀏覽器API方法,樣板ref 是 Vue template樣板元件上的一個特殊屬性,讓開發者可以樣板上直接獲取到 DOM 元素或子組件的實例引用。但從本質上來說,ref 本身並不是直接操作網頁的 DOM 元素,而是 Vue 提供的對應 DOM 或組件實例的參考,具體來說比較是一種映射關係,這樣我們可以在響應式系統下更方便地操作。

onMounted 捕捉初始化真正dom元素映射

一般來說官方會見是使用生命週期鉤子onMounted,它的執行點會在瀏覽器渲染完成後,因此可用來取得Vue幫我們映射好的網頁dom元素vue本身的元件實例,藉由console.log打印你觀察除了發現DOM元素和對應瀏覽器API外,也可能是一個子元件的實例在裡面。

<template>
  <div>
    <!-- ref 會映射到這個 input 元素 -->
    <input ref="inputElement" type="text" />
    <child-component ref="childComponent" />
    <button @click="focusInput">Focus Input</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const inputElement = ref(null);
const childComponent = ref(null);

function focusInput() {
  // 在這裡,我們使用映射來訪問 input 元素並調用它的焦點方法
  inputElement.value.focus();
}

onMounted(() => {
  console.log(inputElement.value);  // 這裡的 value 是 DOM 元素
  console.log(childComponent.value);  // 這裡的 value 是子組件的實例
});
</script>

https://ithelp.ithome.com.tw/upload/images/20240930/20145251ZuGEpHv4hX.png


資料非受控現象(會和響應式系統斷開,不會反向再次更新)

template ref 確實可以用來取得 DOM 元素的直接引用,但它本身並不是響應式的,也就是說,ref 取得的 DOM 元素引用並不會自動受到 Vue 的響應式系統監控。

換句話說,當你使用 template ref 來引用某個 DOM 元素後,雖然你可以對這個 DOM 元素使用一些瀏覽器API進行資料操作,但這些操作的變化不會觸發 Vue 的資料流或響應式更新,像是下方的案例可以玩玩看。

範例:

https://ithelp.ithome.com.tw/upload/images/20240930/20145251pcAR9okTJ4.png


非受控元件(uncontrolled component)

總而言之,非受控元件指的是那些不直接依賴 Vue 所定義的數據資料流ref/reactive)来管理資料顯示狀態。

它們的狀態由瀏覽器自己控制,透過使用者的互動來改變,本身不會進到參與Vue數據管理更新中。

input輸入內容如果沒有透過v-model綁定到ref/reactive中,雖然可以正常打字輸入,但顯示資料是瀏覽器所控制。

  • 表單輸入初始值:

使用者可以修改輸入框的值,但 Vue 不會自動參與該值的更新。

<input type="text" value="initial value">
  • checkbox狀態:

如果你直接使用 checked 來初始化checkbox按鈕的選擇狀態,而不使用 v-model,這些輸入元素的狀態就會變成非受控組件,由瀏覽器資料流自己控制狀態。

<input type="checkbox" checked>

列表渲染(v-for)和input實際案例

剛好找的一篇文章是說明Vue列表渲染和非受控元素之間關系,可以進去玩玩看

範例:

你會發現我們初始化checkbox的資料狀態是我們用Vue響應式資料定義好,將checkbox初始化資料由Vueref先定義掌控,之後我們可以透過點擊checkbox來改變選擇狀態。

但我們製作一個random shuffle功能,去隨意置換v-for陣列的順序,你會發現checkbox狀態好像就不對的???

仔細觀察會有一種我把某些checbox點掉,按下random shuffle置換陣列順序時,又會回到初始化狀態,順序有點打亂對不上的bug。

<template>
   <button @click="shuffleItems">Shuffle Items</button>
  <ul>
    <li v-for="item in items">
      <input type="checkbox" :checked="item.checked">
      {{ item.name }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: 'Apple', checked: true },
  { id: 2, name: 'Banana', checked: false },
  { id: 3, name: 'Cherry', checked: true }
]);
 // 随机打亂列表顺序的函数
function shuffleItems() {
  items.value = items.value.sort(() => Math.random() - 0.5);
}
</script>

問題解析:

  1. 在初次渲染,checkbox狀態由 item.checked 控制,和Vue響應式資料流同步

  2. 在使用者交互後,如果使用者勾選或取消chekbox,這個狀態實際上的改變是由瀏覽器所控制,不會影響Vue響應式資料流 item.checked 的值,這裡没有使用 v-model,會變成非受控的組件狀態

  3. 執行shuffleItems隨機打亂列表 : 因為没有 key 值,預設 Vue 使用就地更新的策略,重複利用了原有的 DOM 元素去渲染綁定的響應式資料,但是瀏覽器上DOM元素的cheked狀態,還是原本的瀏覽器自己掌握的資料流

checkbox可能会因為Vue資料流有設定item.checked 初始化的狀態,但執行shuffleItems功能後,Vue將響應式資料就地更新(in place patch)至原本順序的DOM元素上,並不是反映使用者真正互動操作過後 item.checked 的實際值,是一種兩者資料不同步情況。

  • 關係會類似像這張圖一樣:

  • 瀏覽器非受控元素有自己的狀態,但Vue的資料部採取就地更新值接替換,兩者沒有同步在一起


如何正確排除這種狀況和使用v-for呢?

  1. 使用v-model 綁定checkbox 狀態進入Vue的響應式資料流,變成受控元件(controlled componenet)
  2. 綁定v-for key值,讓Vue DOM更新時知道原本資料和DOM的關係順序。

修正範例:

<template>
   <button @click="shuffleItems">Shuffle Items</button>
  <ul>
    <li v-for="item in items">
      <input type="checkbox" v-model="item.checked" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

v-model 用在表單(如複選框、文本框等)進行雙向數據綁定。當使用者在複選框上點選使用 時,v-model 會將 item 的 checked 屬性狀態和複選框的狀態一起绑定。

綁定 key 的作用:

是在 Vue 中確保 DOM 元素的狀態和綁定的數據一致,並在元素資料的位置或順序(Vue資料流)發生變化時,真正DOM元素能夠依據key值正確地移動和更新,這樣複選框的狀態就會隨著Vue資料流一起變動。

  • 關係會變成像這張圖一樣:

把原本非受控DOM的狀態,加入key 讓Vue之知道說要v-for列表的更新跟著綁定的Vue資料流走:


用陣列index順序當作key值直接綁定是建議作法嗎?

儘管Vue允許index可以作為v-for列表渲染時的 key使用,但它可能在某些情況下導致意想不到的問題。

  • 當數據發生排序變化時或刪除其中一筆資料,像是todoList,數據狀態就有可能錯亂,比較大的問題是因為順序結構改變,整張列表都要一律重新渲染,即便某些資料真的都沒異動。

一律重新渲染的問題:

使用 index 作為 v-for 列表選渲染的 key時,一旦列表的長度發生變化(比如增加或移除項目), Vue 會重新渲染整張列表資料,即便可能只是在資料的頭或尾巴補一筆新資料,可以打開範例使用瀏覽器devtool檢查是否會造成全部列表重新更新渲染一次。

使用 item 的唯一id值作為key後,當你在陣列頭部插入或刪除元素時,Vue 只會重新渲染實際發生變化的部分,而不會重新渲染整個列表。這將會減少 DOM 操作的次數,優化頁面的渲染效能,尤其是在表單資料陣列較大時效果更為顯著。

可以玩玩看有綁定特定id的和index的渲染差別~

範例:

<script setup>
import { ref } from 'vue'

const array = ref(['a','b','c','d'])
const obj = ref([
{
  id: 1,
  val:'a'
},{
  id: 2,
  val:'b'
},{
  id: 3,
  val:'c'
},{
  id: 4,
  val:'d'
}])
const insert = () => {
  array.value.splice(0, 0, 'f')
}
const remove = () => {
  array.value.splice(0, 1)
}

const insertObj = ()=> {
  const add = {
    id: self.crypto.randomUUID(),
    val: 'dd' + self.crypto.randomUUID()
  } 
  obj.value.push(add)
}
const removeObj = () => {
  obj.value.splice(0, 1)
}
</script>

<template>
  <button @click="insert">add f</button>
  <button @click="remove">remove</button>
  <div v-for="(item,index) in array" :key="index">{{item}}</div>
  <button @click="insertObj">add obj</button>
  <button @click="removeObj">remove obj</button>
   <div v-for="(item) in obj" :key="item.id">{{item}}</div>
</template>

總結:

  • 在 Vue 中 v-model 可以使資料數據流完全受控於響應式系統資料中,確保表單輸入和數據模型的同步,較不會出現受控和非受控元素資料流分離情況產生

  • 但光使用 v-model還不足以完全避免 UI 和數據狀態的脫離狀況。為了確保一致性,在使用 v-for 渲染列表時,應避免使用 index 作為 key

  • 因為 index 對應的資料可能是不固定的,而且index 並不是專一性的unique key,當列表發生插入、刪除或排序等操作時,index 會變化進而導致 Vue 無法綁定正確的資料,使用上key 應該選擇一個唯一且穩定的標識符(如 id)。


學習資源

  1. https://medium.com/@seed45699/vue-%E7%9A%84-key-%E5%80%BC%E5%88%A5%E5%86%8D%E7%B6%81-index-%E5%95%A6-5180fa71021
    https://vuejs.org/guide/essentials/list.html#displaying-filtered-sorted-results

  2. https://medium.com/@mkidsc1603/vue3-v-for-key-bind-index-issue-%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90-7cb4826d6c10
    https://deepsource.com/blog/key-attribute-vue-js

  3. https://vueschool.io/articles/vuejs-tutorials/tips-and-gotchas-for-using-key-with-v-for-in-vue-js-3/

  4. https://dev.to/katelynjewel/controlled-vs-uncontrolled-components-44e0


上一篇
Day 16: Vue的無渲染元件 - slot props 的另一種常見用法
下一篇
Day 18: JavaScript 工廠函式 和 類別(class)
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言